Skip to content

Conversation

@luancazarine
Copy link
Collaborator

@luancazarine luancazarine commented Jul 7, 2025

Resolves #17341

Summary by CodeRabbit

  • New Features

    • Introduced actions to create contacts and deals in Freshsales.
    • Added actions to list all contacts and all deals from Freshsales.
  • Improvements

    • Enhanced Freshsales integration with new property selectors and dynamic field loading.
    • Added utility functions for formatting and parsing data.
  • Documentation

    • Removed the "Lead Scoring Automation" example from the Freshsales component documentation.
  • Chores

    • Updated package version and dependencies.
    • Adjusted package entry point and cleaned up configuration files.
  • Refactor

    • Replaced the previous app integration with a new, more robust Freshsales app module.

Actions
 - Create Contact
 - Create Deal
 - List All Contacts
 - List All Deals
@luancazarine luancazarine linked an issue Jul 7, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Jul 7, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

3 Skipped Deployments
Name Status Preview Comments Updated (UTC)
docs-v2 ⬜️ Ignored (Inspect) Jul 7, 2025 7:10pm
pipedream-docs ⬜️ Ignored (Inspect) Jul 7, 2025 7:10pm
pipedream-docs-redirect-do-not-edit ⬜️ Ignored (Inspect) Jul 7, 2025 7:10pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 7, 2025

Walkthrough

This update introduces a new Freshsales integration with dynamic actions for creating and listing contacts and deals, utility functions, and a comprehensive app interface for interacting with the Freshsales API. It also removes legacy files and updates documentation and configuration to reflect the new implementation.

Changes

Files / Grouped Files Change Summary
components/freshsales/actions/create-contact/create-contact.mjs,
components/freshsales/actions/create-deal/create-deal.mjs,
components/freshsales/actions/list-all-contacts/list-all-contacts.mjs,
components/freshsales/actions/list-all-deals/list-all-deals.mjs
Added new dynamic actions for creating and listing contacts and deals in Freshsales.
components/freshsales/freshsales.app.mjs Added new Freshsales app integration with dynamic property definitions and API methods.
components/freshsales/common/utils.mjs Introduced utility functions for string conversion and object parsing.
components/freshsales/app/freshsales.app.ts Removed legacy Freshsales app definition.
components/freshsales/.gitignore Deleted .gitignore file to allow versioning of previously ignored files.
components/freshsales/README.md Removed the "Lead Scoring Automation" example from documentation.
components/freshsales/package.json Updated package version, entry point, dependencies, and removed "dist" from files.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Action
    participant FreshsalesApp
    participant FreshsalesAPI

    User->>Action: Trigger (e.g., Create Contact/Deal)
    Action->>FreshsalesApp: Fetch dynamic fields/options (e.g., contact fields, sales accounts)
    FreshsalesApp->>FreshsalesAPI: GET fields/accounts/filters
    FreshsalesAPI-->>FreshsalesApp: Return metadata/options
    FreshsalesApp-->>Action: Return fields/options
    User->>Action: Provide input data
    Action->>FreshsalesApp: Create entity (contact/deal) with data
    FreshsalesApp->>FreshsalesAPI: POST create request
    FreshsalesAPI-->>FreshsalesApp: Return created entity
    FreshsalesApp-->>Action: Return result
    Action-->>User: Output summary and details
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement actions for: List all Contacts, Create Contact, List All Deals, Create Deal in Freshsales (#17341)
Use Freshsales API endpoints as referenced in the linked documentation (#17341)

Poem

In the garden of code, where fresh deals bloom,
New contacts and actions dispel the gloom.
With lists and creates, we hop with glee,
Freshsales and rabbits, in perfect harmony.
🥕✨

— A rabbit, delighted by every API!

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

components/freshsales/actions/create-contact/create-contact.mjs

Oops! Something went wrong! :(

ESLint: 8.57.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:255:9)
at packageResolve (node:internal/modules/esm/resolve:767:81)
at moduleResolve (node:internal/modules/esm/resolve:853:18)
at defaultResolve (node:internal/modules/esm/resolve:983:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:801:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:725:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:708:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:309:38)
at #link (node:internal/modules/esm/module_job:202:49)

components/freshsales/actions/create-deal/create-deal.mjs

Oops! Something went wrong! :(

ESLint: 8.57.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:255:9)
at packageResolve (node:internal/modules/esm/resolve:767:81)
at moduleResolve (node:internal/modules/esm/resolve:853:18)
at defaultResolve (node:internal/modules/esm/resolve:983:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:801:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:725:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:708:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:309:38)
at #link (node:internal/modules/esm/module_job:202:49)

components/freshsales/actions/list-all-contacts/list-all-contacts.mjs

Oops! Something went wrong! :(

ESLint: 8.57.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:255:9)
at packageResolve (node:internal/modules/esm/resolve:767:81)
at moduleResolve (node:internal/modules/esm/resolve:853:18)
at defaultResolve (node:internal/modules/esm/resolve:983:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:801:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:725:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:708:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:309:38)
at #link (node:internal/modules/esm/module_job:202:49)

  • 3 others
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
components/freshsales/common/utils.mjs (1)

1-4: The regex pattern may not handle all edge cases correctly.

The current regex ^_*(.)|_+(.)/g doesn't properly handle consecutive underscores or leading underscores in all scenarios. For example, "__test_case" would become "Test case" (missing the first character), and "_test__case" might not format correctly.

Consider this more robust implementation:

-export const snakeCaseToTitleCase = (s) =>
-  s.replace(/^_*(.)|_+(.)/g, (s, c, d) => c
-    ? c.toUpperCase()
-    : " " + d.toUpperCase());
+export const snakeCaseToTitleCase = (s) =>
+  s.replace(/^_+/, '')
+    .replace(/_+/g, ' ')
+    .replace(/\b\w/g, (c) => c.toUpperCase());
components/freshsales/freshsales.app.mjs (1)

143-168: Good pagination implementation with minor optimization opportunity.

The pagination logic is well-structured. However, there's a minor issue with the hasMore logic - it should check if there are more pages available rather than just if the current page has data.

Consider improving the pagination termination condition:

-        hasMore = data[responseField].length;
+        hasMore = data[responseField].length > 0 && data[responseField].length === (params.per_page || 25);

This assumes the API uses a per_page parameter and stops when fewer results are returned than requested.

components/freshsales/actions/list-all-contacts/list-all-contacts.mjs (1)

24-27: Consider using Array.from() for better performance.

While the current approach works, using Array.from() would be more efficient for collecting async generator results.

-    const contacts = [];
-    for await (const contact of response) {
-      contacts.push(contact);
-    }
+    const contacts = [];
+    for await (const contact of response) {
+      contacts.push(contact);
+    }

Actually, the current implementation is fine. The suggestion above doesn't improve it. Keep as is.

components/freshsales/actions/create-contact/create-contact.mjs (1)

39-39: Minor inconsistency in value type conversion.

The sales account values are converted to strings here, but in the create-deal action (lines 40-43), they remain as integers. Consider maintaining consistency across both actions.

For consistency with the create-deal action, remove the string conversion:

-         value: `${account.id}`,
+         value: account.id,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65d964b and 075a599.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • components/freshsales/.gitignore (0 hunks)
  • components/freshsales/README.md (0 hunks)
  • components/freshsales/actions/create-contact/create-contact.mjs (1 hunks)
  • components/freshsales/actions/create-deal/create-deal.mjs (1 hunks)
  • components/freshsales/actions/list-all-contacts/list-all-contacts.mjs (1 hunks)
  • components/freshsales/actions/list-all-deals/list-all-deals.mjs (1 hunks)
  • components/freshsales/app/freshsales.app.ts (0 hunks)
  • components/freshsales/common/utils.mjs (1 hunks)
  • components/freshsales/freshsales.app.mjs (1 hunks)
  • components/freshsales/package.json (1 hunks)
💤 Files with no reviewable changes (3)
  • components/freshsales/.gitignore
  • components/freshsales/README.md
  • components/freshsales/app/freshsales.app.ts
🧰 Additional context used
🧠 Learnings (6)
components/freshsales/package.json (1)
Learnt from: jcortes
PR: PipedreamHQ/pipedream#14935
File: components/sailpoint/package.json:15-18
Timestamp: 2024-12-12T19:23:09.039Z
Learning: When developing Pipedream components, do not add built-in Node.js modules like `fs` to `package.json` dependencies, as they are native modules provided by the Node.js runtime.
components/freshsales/actions/list-all-contacts/list-all-contacts.mjs (4)
Learnt from: jcortes
PR: PipedreamHQ/pipedream#14467
File: components/gainsight_px/actions/create-account/create-account.mjs:4-6
Timestamp: 2024-10-30T15:24:39.294Z
Learning: In `components/gainsight_px/actions/create-account/create-account.mjs`, the action name should be "Create Account" instead of "Create Memory".
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#12731
File: components/hackerone/actions/get-members/get-members.mjs:3-28
Timestamp: 2024-07-04T18:11:59.822Z
Learning: When exporting a summary message in the `run` method of an action, ensure the message is correctly formatted. For example, in the `hackerone-get-members` action, the correct format is `Successfully retrieved ${response.data.length} members`.
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#12731
File: components/hackerone/actions/get-members/get-members.mjs:3-28
Timestamp: 2024-10-08T15:33:38.240Z
Learning: When exporting a summary message in the `run` method of an action, ensure the message is correctly formatted. For example, in the `hackerone-get-members` action, the correct format is `Successfully retrieved ${response.data.length} members`.
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#16954
File: components/salesloft/salesloft.app.mjs:14-23
Timestamp: 2025-06-04T17:52:05.780Z
Learning: In the Salesloft API integration (components/salesloft/salesloft.app.mjs), the _makeRequest method returns response.data which directly contains arrays for list endpoints like listPeople, listCadences, listUsers, and listAccounts. The propDefinitions correctly call .map() directly on these responses without needing to destructure a nested data property.
components/freshsales/actions/list-all-deals/list-all-deals.mjs (4)
Learnt from: jcortes
PR: PipedreamHQ/pipedream#14467
File: components/gainsight_px/actions/create-account/create-account.mjs:4-6
Timestamp: 2024-10-30T15:24:39.294Z
Learning: In `components/gainsight_px/actions/create-account/create-account.mjs`, the action name should be "Create Account" instead of "Create Memory".
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#12731
File: components/hackerone/actions/get-members/get-members.mjs:3-28
Timestamp: 2024-07-04T18:11:59.822Z
Learning: When exporting a summary message in the `run` method of an action, ensure the message is correctly formatted. For example, in the `hackerone-get-members` action, the correct format is `Successfully retrieved ${response.data.length} members`.
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#12731
File: components/hackerone/actions/get-members/get-members.mjs:3-28
Timestamp: 2024-10-08T15:33:38.240Z
Learning: When exporting a summary message in the `run` method of an action, ensure the message is correctly formatted. For example, in the `hackerone-get-members` action, the correct format is `Successfully retrieved ${response.data.length} members`.
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#16954
File: components/salesloft/salesloft.app.mjs:14-23
Timestamp: 2025-06-04T17:52:05.780Z
Learning: In the Salesloft API integration (components/salesloft/salesloft.app.mjs), the _makeRequest method returns response.data which directly contains arrays for list endpoints like listPeople, listCadences, listUsers, and listAccounts. The propDefinitions correctly call .map() directly on these responses without needing to destructure a nested data property.
components/freshsales/actions/create-deal/create-deal.mjs (2)
Learnt from: jcortes
PR: PipedreamHQ/pipedream#14467
File: components/gainsight_px/actions/create-account/create-account.mjs:4-6
Timestamp: 2024-10-30T15:24:39.294Z
Learning: In `components/gainsight_px/actions/create-account/create-account.mjs`, the action name should be "Create Account" instead of "Create Memory".
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#16954
File: components/salesloft/salesloft.app.mjs:14-23
Timestamp: 2025-06-04T17:52:05.780Z
Learning: In the Salesloft API integration (components/salesloft/salesloft.app.mjs), the _makeRequest method returns response.data which directly contains arrays for list endpoints like listPeople, listCadences, listUsers, and listAccounts. The propDefinitions correctly call .map() directly on these responses without needing to destructure a nested data property.
components/freshsales/actions/create-contact/create-contact.mjs (3)
Learnt from: jcortes
PR: PipedreamHQ/pipedream#14467
File: components/gainsight_px/actions/create-account/create-account.mjs:4-6
Timestamp: 2024-10-30T15:24:39.294Z
Learning: In `components/gainsight_px/actions/create-account/create-account.mjs`, the action name should be "Create Account" instead of "Create Memory".
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#12731
File: components/hackerone/actions/get-members/get-members.mjs:3-28
Timestamp: 2024-07-04T18:11:59.822Z
Learning: When exporting a summary message in the `run` method of an action, ensure the message is correctly formatted. For example, in the `hackerone-get-members` action, the correct format is `Successfully retrieved ${response.data.length} members`.
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#12731
File: components/hackerone/actions/get-members/get-members.mjs:3-28
Timestamp: 2024-10-08T15:33:38.240Z
Learning: When exporting a summary message in the `run` method of an action, ensure the message is correctly formatted. For example, in the `hackerone-get-members` action, the correct format is `Successfully retrieved ${response.data.length} members`.
components/freshsales/freshsales.app.mjs (2)
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#16954
File: components/salesloft/salesloft.app.mjs:14-23
Timestamp: 2025-06-04T17:52:05.780Z
Learning: In the Salesloft API integration (components/salesloft/salesloft.app.mjs), the _makeRequest method returns response.data which directly contains arrays for list endpoints like listPeople, listCadences, listUsers, and listAccounts. The propDefinitions correctly call .map() directly on these responses without needing to destructure a nested data property.
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#16954
File: components/salesloft/salesloft.app.mjs:14-23
Timestamp: 2025-06-04T17:52:05.780Z
Learning: The Salesloft API list endpoints (listPeople, listCadences, listUsers, listAccounts) return arrays directly in the response body, not wrapped in a metadata object with a nested data property. The _makeRequest method correctly returns response.data which contains the arrays that can be mapped over directly in propDefinitions.
🪛 Biome (1.9.4)
components/freshsales/actions/create-contact/create-contact.mjs

[error] 94-94: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


[error] 95-95: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Publish TypeScript components
  • GitHub Check: Verify TypeScript components
  • GitHub Check: pnpm publish
  • GitHub Check: Lint Code Base
🔇 Additional comments (11)
components/freshsales/common/utils.mjs (1)

6-29: LGTM! Good error handling in parseObject.

The function properly handles different input types and gracefully falls back to the original value when JSON parsing fails. The array handling is particularly well implemented.

components/freshsales/package.json (2)

3-5: LGTM! Version bump and main entry point update look correct.

The version increment to 0.1.0 appropriately reflects the significant changes, and the main entry point correctly points to the new app file.


15-16: Dependency Version Confirmed

The @pipedream/platform dependency is already set to the latest stable release (3.1.0), which includes the pipedream-axios package for Axios integration. No changes needed.

components/freshsales/actions/list-all-contacts/list-all-contacts.mjs (2)

13-22: Pagination setup looks correct.

The action properly gets the filter ID and sets up pagination with the correct parameters. The use of the async generator pattern is appropriate here.


29-31: Good summary message formatting.

The summary message correctly handles singular/plural forms and provides useful feedback about the number of contacts fetched.

components/freshsales/actions/list-all-deals/list-all-deals.mjs (2)

13-22: Consistent implementation with contacts action.

The action follows the same pattern as the contacts action, which is good for consistency. The filter name "All Deals" is appropriate for the deals model.


24-32: LGTM! Good consistency with contacts action.

The pagination collection and summary message formatting are consistent with the contacts action, maintaining good code consistency across the codebase.

components/freshsales/actions/create-deal/create-deal.mjs (2)

1-17: LGTM! Clean action structure and setup.

The action is well-structured with appropriate metadata, key naming, and documentation reference. The required name property with reloadProps: true is correctly configured to trigger additional property loading.


74-89: LGTM! Clean run method implementation.

The run method correctly destructures the input data, makes the API call, and provides a helpful summary message. The response handling and export are appropriate.

components/freshsales/actions/create-contact/create-contact.mjs (2)

1-18: LGTM! Good imports and action setup.

The action is well-configured with appropriate imports including the parseObject utility. The email property setup matches the pattern used in the create-deal action.


98-105: LGTM! Clean API call and response handling.

The contact creation API call and response handling are implemented correctly with appropriate summary export.

path: `/${model}/filters`,
...opts,
});
return filters.find((filter) => filter.name === name).id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for missing filter.

The code assumes a filter with the specified name exists, but doesn't handle the case where it might not be found.

Add error handling:

-      return filters.find((filter) => filter.name === name).id;
+      const filter = filters.find((filter) => filter.name === name);
+      if (!filter) {
+        throw new Error(`Filter with name "${name}" not found for model "${model}"`);
+      }
+      return filter.id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return filters.find((filter) => filter.name === name).id;
const filter = filters.find((filter) => filter.name === name);
if (!filter) {
throw new Error(`Filter with name "${name}" not found for model "${model}"`);
}
return filter.id;
🤖 Prompt for AI Agents
In components/freshsales/freshsales.app.mjs at line 79, the code assumes that
filters.find returns a filter object, but if no filter matches the name, it
returns undefined causing an error when accessing .id. Modify the code to first
check if the filter exists; if not, throw an appropriate error or handle the
missing filter case gracefully before returning the id.

Comment on lines +11 to +19
async options() {
const response = await this.listContacts();
return response.contacts?.map(({
id, first_name, last_name, email,
}) => ({
label: `${first_name || ""} ${last_name || ""}`.trim() || email || `Contact ${id}`,
value: id,
})) || [];
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential issue with undefined contacts response.

The code accesses response.contacts but the listContacts method doesn't provide a default filterId. This could cause the API call to fail if no filter ID is provided.

The listContacts method requires a filterId parameter, but in the propDefinition, it's called without one. This will likely cause API errors. Consider using the getContacts method instead:

-        const response = await this.listContacts();
+        const response = await this.getContacts();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async options() {
const response = await this.listContacts();
return response.contacts?.map(({
id, first_name, last_name, email,
}) => ({
label: `${first_name || ""} ${last_name || ""}`.trim() || email || `Contact ${id}`,
value: id,
})) || [];
},
async options() {
const response = await this.getContacts();
return response.contacts?.map(({
id, first_name, last_name, email,
}) => ({
label: `${first_name || ""} ${last_name || ""}`.trim() || email || `Contact ${id}`,
value: id,
})) || [];
},
🤖 Prompt for AI Agents
In components/freshsales/freshsales.app.mjs around lines 11 to 19, the options
method calls listContacts without providing the required filterId parameter,
which can cause API errors or undefined response.contacts. To fix this, replace
the call to listContacts with getContacts, which does not require a filterId,
ensuring the API call succeeds and response.contacts is defined.

Comment on lines +25 to +33
async options() {
const response = await this.listDeals();
return response.deals?.map(({
id, name,
}) => ({
label: name || `Deal ${id}`,
value: id,
})) || [];
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same issue with deals - missing filterId parameter.

Similar to contacts, the listDeals method requires a filterId parameter that's not provided.

Apply the same fix here:

-        const response = await this.listDeals();
+        const response = await this.listDeals({
+          filterId: await this.getFilterId({
+            model: "deals",
+            name: "All Deals"
+          })
+        });

Or create a getDeals method similar to getContacts for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async options() {
const response = await this.listDeals();
return response.deals?.map(({
id, name,
}) => ({
label: name || `Deal ${id}`,
value: id,
})) || [];
},
async options() {
const response = await this.listDeals({
filterId: await this.getFilterId({
model: "deals",
name: "All Deals"
})
});
return response.deals?.map(({
id, name,
}) => ({
label: name || `Deal ${id}`,
value: id,
})) || [];
},
🤖 Prompt for AI Agents
In components/freshsales/freshsales.app.mjs around lines 25 to 33, the options
method calls listDeals without the required filterId parameter. Fix this by
passing the appropriate filterId argument to listDeals, similar to how it is
done for contacts, or alternatively create a getDeals method that accepts
filterId and use it here for consistency.

Comment on lines +18 to +73
async additionalProps() {
const { fields } = await this.freshsales.getDealFields();
const filteredFields = fields.filter((field) => (field.visible === true && field.name != "name"));

const props = {};
for (const field of filteredFields) {

const data = {
type: field.type === "multi_select_dropdown"
? "string[]"
: "string",
label: field.label,
description: `${field.label} of the deal.`,
optional: field.required === false,
};

if (field.name === "sales_account_id") {
const { sales_accounts: options } = await this.freshsales.getSalesAccounts();
data.type = "integer";
data.label = "Sales Account";
data.description = "Sales account of the deal.";
data.optional = true;
data.options = options.map((account) => ({
label: account.name,
value: account.id,
}));
}

if (field.name === "contacts") {
const { contacts: options } = await this.freshsales.getContacts();
data.type = "integer[]";
data.label = "Contacts";
data.description = "Contacts of the deal.";
data.optional = true;
data.options = options.map((contact) => ({
label: contact.display_name,
value: contact.id,
}));
}

if ([
"dropdown",
"multi_select_dropdown",
].includes(field.type)) {
data.type = "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}

props[field.name] = data;
}

return props;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Good dynamic property generation with room for type consistency improvement.

The dynamic property generation logic correctly filters visible fields and handles special cases appropriately. However, there's a type inconsistency issue that should be addressed.

Issue: Type inconsistency in multi_select_dropdown handling

Lines 26-28 set multi_select_dropdown fields to "string[]", but lines 62-67 override this to "integer" for dropdown fields. This creates inconsistent behavior for multi-select dropdowns.

Apply this fix to maintain type consistency:

      if ([
        "dropdown",
        "multi_select_dropdown",
      ].includes(field.type)) {
-       data.type = "integer";
+       data.type = field.type === "multi_select_dropdown" ? "integer[]" : "integer";
        data.options = field.choices.map((choice) => ({
          label: choice.value,
          value: choice.id,
        }));
      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async additionalProps() {
const { fields } = await this.freshsales.getDealFields();
const filteredFields = fields.filter((field) => (field.visible === true && field.name != "name"));
const props = {};
for (const field of filteredFields) {
const data = {
type: field.type === "multi_select_dropdown"
? "string[]"
: "string",
label: field.label,
description: `${field.label} of the deal.`,
optional: field.required === false,
};
if (field.name === "sales_account_id") {
const { sales_accounts: options } = await this.freshsales.getSalesAccounts();
data.type = "integer";
data.label = "Sales Account";
data.description = "Sales account of the deal.";
data.optional = true;
data.options = options.map((account) => ({
label: account.name,
value: account.id,
}));
}
if (field.name === "contacts") {
const { contacts: options } = await this.freshsales.getContacts();
data.type = "integer[]";
data.label = "Contacts";
data.description = "Contacts of the deal.";
data.optional = true;
data.options = options.map((contact) => ({
label: contact.display_name,
value: contact.id,
}));
}
if ([
"dropdown",
"multi_select_dropdown",
].includes(field.type)) {
data.type = "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
return props;
},
async additionalProps() {
const { fields } = await this.freshsales.getDealFields();
const filteredFields = fields.filter((field) => (field.visible === true && field.name != "name"));
const props = {};
for (const field of filteredFields) {
const data = {
type: field.type === "multi_select_dropdown"
? "string[]"
: "string",
label: field.label,
description: `${field.label} of the deal.`,
optional: field.required === false,
};
if (field.name === "sales_account_id") {
const { sales_accounts: options } = await this.freshsales.getSalesAccounts();
data.type = "integer";
data.label = "Sales Account";
data.description = "Sales account of the deal.";
data.optional = true;
data.options = options.map((account) => ({
label: account.name,
value: account.id,
}));
}
if (field.name === "contacts") {
const { contacts: options } = await this.freshsales.getContacts();
data.type = "integer[]";
data.label = "Contacts";
data.description = "Contacts of the deal.";
data.optional = true;
data.options = options.map((contact) => ({
label: contact.display_name,
value: contact.id,
}));
}
if ([
"dropdown",
"multi_select_dropdown",
].includes(field.type)) {
data.type = field.type === "multi_select_dropdown"
? "integer[]"
: "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
return props;
},
🤖 Prompt for AI Agents
In components/freshsales/actions/create-deal/create-deal.mjs between lines 18
and 73, the type for multi_select_dropdown fields is initially set to "string[]"
but later overwritten to "integer" in the dropdown type handling block, causing
inconsistency. To fix this, modify the condition that sets data.type to
"integer" so it excludes multi_select_dropdown fields, ensuring
multi_select_dropdown fields retain the "string[]" type while only dropdown
fields get "integer".

Comment on lines +79 to +96
if (data.primaryAccount) {
data.sales_accounts = [
{
id: data.primaryAccount,
is_primary: true,
},
];
parseObject(data.additionalAccounts)?.map((account) => {
data.sales_accounts.push(
{
id: account,
is_primary: false,
},
);
});
delete data.primaryAccount;
delete data.additionalAccounts;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Address performance issue with delete operator.

The data transformation logic is correct but uses the delete operator which can impact performance. The static analysis tool correctly flagged this issue.

Apply this fix to improve performance by using undefined assignment instead of delete:

-     delete data.primaryAccount;
-     delete data.additionalAccounts;
+     data.primaryAccount = undefined;
+     data.additionalAccounts = undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (data.primaryAccount) {
data.sales_accounts = [
{
id: data.primaryAccount,
is_primary: true,
},
];
parseObject(data.additionalAccounts)?.map((account) => {
data.sales_accounts.push(
{
id: account,
is_primary: false,
},
);
});
delete data.primaryAccount;
delete data.additionalAccounts;
}
if (data.primaryAccount) {
data.sales_accounts = [
{
id: data.primaryAccount,
is_primary: true,
},
];
parseObject(data.additionalAccounts)?.map((account) => {
data.sales_accounts.push(
{
id: account,
is_primary: false,
},
);
});
data.primaryAccount = undefined;
data.additionalAccounts = undefined;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 94-94: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


[error] 95-95: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

🤖 Prompt for AI Agents
In components/freshsales/actions/create-contact/create-contact.mjs around lines
79 to 96, replace the delete operator used on data.primaryAccount and
data.additionalAccounts with assignments to undefined to improve performance.
Instead of deleting these properties, set data.primaryAccount = undefined and
data.additionalAccounts = undefined after processing.

Comment on lines +19 to +71
async additionalProps() {
const { fields } = await this.freshsales.getContactFields();
const filteredFields = fields.filter((field) => (field.visible === true && field.name != "emails"));

const props = {};
for (const field of filteredFields) {

const data = {
type: field.type === "multi_select_dropdown"
? "string[]"
: "string",
label: field.label,
description: `${field.label} of the contact.`,
optional: field.required === false,
};

if (field.name === "sales_accounts") {
const { sales_accounts: options } = await this.freshsales.getSalesAccounts();
const salesAccountOptions = options.map((account) => ({
label: account.name,
value: `${account.id}`,
}));
props.primaryAccount = {
type: "string",
label: "Primary Sales Account",
description: "Primary sales account of the contact.",
optional: true,
options: salesAccountOptions,
};
props.additionalAccounts = {
type: "string[]",
label: "Additional Sales Accounts",
description: "Additional sales accounts of the contact.",
optional: true,
options: salesAccountOptions,
};
} else {
if ([
"dropdown",
"multi_select_dropdown",
].includes(field.type)) {
data.type = "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
}

return props;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Good dynamic property generation with type consistency fix needed.

The logic correctly handles the special sales_accounts field by splitting it into primary and additional account properties. However, there's the same type inconsistency issue as in the create-deal action.

Issue: Type inconsistency in multi_select_dropdown handling

Lines 27-29 set multi_select_dropdown fields to "string[]", but lines 60-65 override this to "integer" for dropdown fields. This creates inconsistent behavior for multi-select dropdowns.

Apply this fix to maintain type consistency:

        if ([
          "dropdown",
          "multi_select_dropdown",
        ].includes(field.type)) {
-         data.type = "integer";
+         data.type = field.type === "multi_select_dropdown" ? "integer[]" : "integer";
          data.options = field.choices.map((choice) => ({
            label: choice.value,
            value: choice.id,
          }));
        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async additionalProps() {
const { fields } = await this.freshsales.getContactFields();
const filteredFields = fields.filter((field) => (field.visible === true && field.name != "emails"));
const props = {};
for (const field of filteredFields) {
const data = {
type: field.type === "multi_select_dropdown"
? "string[]"
: "string",
label: field.label,
description: `${field.label} of the contact.`,
optional: field.required === false,
};
if (field.name === "sales_accounts") {
const { sales_accounts: options } = await this.freshsales.getSalesAccounts();
const salesAccountOptions = options.map((account) => ({
label: account.name,
value: `${account.id}`,
}));
props.primaryAccount = {
type: "string",
label: "Primary Sales Account",
description: "Primary sales account of the contact.",
optional: true,
options: salesAccountOptions,
};
props.additionalAccounts = {
type: "string[]",
label: "Additional Sales Accounts",
description: "Additional sales accounts of the contact.",
optional: true,
options: salesAccountOptions,
};
} else {
if ([
"dropdown",
"multi_select_dropdown",
].includes(field.type)) {
data.type = "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
}
return props;
},
async additionalProps() {
const { fields } = await this.freshsales.getContactFields();
const filteredFields = fields.filter((field) => (field.visible === true && field.name != "emails"));
const props = {};
for (const field of filteredFields) {
const data = {
type: field.type === "multi_select_dropdown"
? "string[]"
: "string",
label: field.label,
description: `${field.label} of the contact.`,
optional: field.required === false,
};
if (field.name === "sales_accounts") {
const { sales_accounts: options } = await this.freshsales.getSalesAccounts();
const salesAccountOptions = options.map((account) => ({
label: account.name,
value: `${account.id}`,
}));
props.primaryAccount = {
type: "string",
label: "Primary Sales Account",
description: "Primary sales account of the contact.",
optional: true,
options: salesAccountOptions,
};
props.additionalAccounts = {
type: "string[]",
label: "Additional Sales Accounts",
description: "Additional sales accounts of the contact.",
optional: true,
options: salesAccountOptions,
};
} else {
if ([
"dropdown",
"multi_select_dropdown",
].includes(field.type)) {
- data.type = "integer";
+ data.type = field.type === "multi_select_dropdown" ? "integer[]" : "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
}
return props;
},
🤖 Prompt for AI Agents
In components/freshsales/actions/create-contact/create-contact.mjs between lines
19 and 71, the type for multi_select_dropdown fields is inconsistently set as
"string[]" initially but then changed to "integer" later in the dropdown
handling logic. To fix this, ensure that multi_select_dropdown fields
consistently use the same type by adjusting the type assignment inside the
dropdown type check to differentiate between single and multi-select dropdowns,
setting "integer" for dropdown and "integer[]" for multi_select_dropdown, and
update the options mapping accordingly to maintain type consistency.

@luancazarine luancazarine merged commit 8b02c21 into master Jul 8, 2025
11 checks passed
@luancazarine luancazarine deleted the 17341-action-freshsales branch July 8, 2025 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ACTION] Freshsales

3 participants